#include <stdio.h>
/*
  Codec2 Proof Of Concept receiver

  Copyright (C) 2011,2012 Peter Lawrence

  All rights reserved.

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License version 2.1, as
  published by the Free Software Foundation.  This program is
  distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with this program; if not, see <http://www.gnu.org/licenses/>.
*/

#include <windows.h>
#include <assert.h>
#include "resource.h"
#include "codec2.h"
#include "modem.h"
#include "config.h"

#define RCV_AUDIO_SAMPLES 4800
#define MAX_DEMOD_SAMPLES 1216
#define LISTEN_BUFFER_SIZE ((2 * MAX_DEMOD_SAMPLES + RCV_AUDIO_SAMPLES) * sizeof(short))

//#define LOG_AUDIO

/* struct of the user's creation that newqpsk passes a pointer back through audioread() */
struct modemchannel
{
	/* struct used by the modem library */
	struct demodulator *demod;
	/* current outstanding input audio buffer */
	short *listen_buffer;
	unsigned int read_index, write_index;
	/* audio position, as used by audioread() and audiocurtime() */
	u_int16_t pos;
	/* buffer to store the Codec2 encoded speech */
	unsigned char *c2_frame;
	/* buffer to store the Codec2 decoded speech */
	short *c2_speech;
	/* flags indicating carrier detect and whether decoded speech is in c2_speech */
	int dcd, laden;
};

/* general function prototypes */
BOOL CALLBACK DlgProc (HWND, UINT, WPARAM, LPARAM);

/* expected by newqpsk library */
unsigned int log_verblevel;

/* local function prototypes */
static void fill_wavein_combo_box(HWND hwnd);
static void fill_waveout_combo_box(HWND hwnd);
static void resize_combo_box(HWND hwnd, int identifier, int count);

/* local constants */
static const TCHAR szAppName[]   = TEXT ("Codec2 RCVR v0.3");
static const TCHAR szOpenError[] = TEXT ("Error opening soundcard!");
static const TCHAR szMemError[]  = TEXT ("Error allocating memory!");
static const TCHAR szC2Error[]   = TEXT ("Codec2 initialization failed!");
/* raw_bps, obsolete, obsolete, tune_len, sync_len */
static const char *params[5] = { "2500", NULL, NULL, "0", "0" };

/* local variables */
static void *codec2_ctx;
static void *modem_ctx;
static struct modemchannel chan;
#ifdef LOG_AUDIO
	static FILE *fd;
#endif

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
	if (-1 == DialogBox (hInstance, TEXT ("C2POC"), NULL, DlgProc))
	{
		MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR);
	}

	return 0;
}

BOOL CALLBACK DlgProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static BOOL bActive, bTerminating;
	static HWAVEIN hWaveIn;
	static HWAVEOUT hWaveOut;
	static PBYTE pBuffer1, pBuffer2;
	static PWAVEHDR pWaveHdr1, pWaveHdr2, pWaveOutHdr, pWaveParmHdr;
	static WAVEFORMATEX waveform;
	static UINT wavein_deviceid, waveout_deviceid;

	static int iteration;
	unsigned int i, index;
	static int audio_buffer_size;
	unsigned int sample_rate, bps;
	short *c2in, *c2out;

	switch (message)
	{
	case WM_INITDIALOG:

		SetWindowText(hwnd, szAppName);

		/* allocate memory for wavIn headers */
		pWaveHdr1 = malloc (sizeof (WAVEHDR));
		pWaveHdr2 = malloc (sizeof (WAVEHDR));

		/* initialize instance of Codec2 */
		codec2_ctx = codec2_create(CODEC2_MODE_2500);

		/* determine worst case Codec2 frame size */
		i = codec2_bits_per_frame(codec2_ctx);

		/* allocate memory for Codec2 frame size */
		chan.c2_frame = malloc((i + 7) / 8);

		/* determine worst case Codec2 samples per frame */
		audio_buffer_size = codec2_samples_per_frame(codec2_ctx);
		
		/* scale value from units of samples to bytes */
		audio_buffer_size *= sizeof(short);

		audio_buffer_size = audio_buffer_size * OFDM_SAMPLE_RATE / 8000;

		/* allocate memory for Codec2 decoded speech */
		chan.c2_speech = (short *)malloc(audio_buffer_size);

		/* allocate memory for listen buffer */
		chan.listen_buffer = (short *)malloc(LISTEN_BUFFER_SIZE);

		/* select the demodulator of choice (this is a legacy of the soundmodem library) */
		chan.demod = &newqpskdemodulator;

		/* config (and be informed of the sample rate) */
		modem_ctx = chan.demod->config(&chan, &sample_rate, params);

		/*
		ignore what the library wants; this is the sample rate we'll use
		*/
		sample_rate = OFDM_SAMPLE_RATE;

		/* init comes after config (and tells us the data rate) */
		chan.demod->init(modem_ctx, sample_rate, &bps);

		/* initialize audio input choices, picking default */
		fill_wavein_combo_box(hwnd);
		SendMessage(GetDlgItem(hwnd, IDC_CHOOSE_IN), CB_SETCURSEL, 0, 0);

		/* initialize audio output choices, picking default */
		fill_waveout_combo_box(hwnd);
		SendMessage(GetDlgItem(hwnd, IDC_CHOOSE_OUT), CB_SETCURSEL, 0, 0);

		/* initialize for later use */
		waveform.wFormatTag      = WAVE_FORMAT_PCM;
		waveform.nChannels       = 1;
		waveform.nSamplesPerSec  = OFDM_SAMPLE_RATE;
		waveform.nAvgBytesPerSec = OFDM_SAMPLE_RATE * sizeof(short);
		waveform.nBlockAlign     = 1;
		waveform.wBitsPerSample  = 16;
		waveform.cbSize          = 0;

		return TRUE;

	case WM_COMMAND:
		switch (LOWORD (wParam))
		{
		case IDC_START:
			/* allocate buffer memory for input audio; software will ping-pong between these two buffers */
			pBuffer1 = malloc (RCV_AUDIO_SAMPLES * sizeof(short));
			pBuffer2 = malloc (RCV_AUDIO_SAMPLES * sizeof(short));

			/* alert user if there is a problem */
			if (!pBuffer1 || !pBuffer2 || !chan.c2_frame || !chan.c2_speech || !chan.listen_buffer)
			{
				if (pBuffer1) free (pBuffer1);
				if (pBuffer2) free (pBuffer2);

				MessageBeep (MB_ICONEXCLAMATION);
				MessageBox (hwnd, szMemError, szAppName, MB_ICONEXCLAMATION | MB_OK);
				return TRUE;
			}

			/* alert user if there was a problem with Codec2 */
			if (!codec2_ctx)
			{
				MessageBeep (MB_ICONEXCLAMATION);
				MessageBox (hwnd, szC2Error, szAppName, MB_ICONEXCLAMATION | MB_OK);
				return TRUE;
			}

			/* open input audio and alert user if there is a problem */

			if (waveInOpen (&hWaveIn, (wavein_deviceid) ? (wavein_deviceid - 1) : WAVE_MAPPER, &waveform, (DWORD) hwnd, 0, CALLBACK_WINDOW))
			{
				MessageBeep (MB_ICONEXCLAMATION);
				MessageBox (hwnd, szOpenError, szAppName, MB_ICONEXCLAMATION | MB_OK);
			}

			/* prepare waveform headers (one for each of the two buffers) */

			pWaveHdr1->lpData          = pBuffer1;
			pWaveHdr1->dwBufferLength  = RCV_AUDIO_SAMPLES * sizeof(short);
			pWaveHdr1->dwBytesRecorded = 0;
			pWaveHdr1->dwUser          = 0;
			pWaveHdr1->dwFlags         = 0;
			pWaveHdr1->dwLoops         = 1;
			pWaveHdr1->lpNext          = NULL;
			pWaveHdr1->reserved        = 0;

			waveInPrepareHeader (hWaveIn, pWaveHdr1, sizeof (WAVEHDR));

			pWaveHdr2->lpData          = pBuffer2;
			pWaveHdr2->dwBufferLength  = RCV_AUDIO_SAMPLES * sizeof(short);
			pWaveHdr2->dwBytesRecorded = 0;
			pWaveHdr2->dwUser          = 0;
			pWaveHdr2->dwFlags         = 0;
			pWaveHdr2->dwLoops         = 1;
			pWaveHdr2->lpNext          = NULL;
			pWaveHdr2->reserved        = 0;

			waveInPrepareHeader (hWaveIn, pWaveHdr2, sizeof (WAVEHDR));

			/* open output audio and alert user if there is a problem */

			if (waveOutOpen (&hWaveOut, (waveout_deviceid) ? (waveout_deviceid - 1) : WAVE_MAPPER, &waveform, (DWORD) hwnd, 0, CALLBACK_WINDOW))
			{
				MessageBeep (MB_ICONEXCLAMATION);
				MessageBox (hwnd, szOpenError, szAppName, MB_ICONEXCLAMATION | MB_OK);
			}
			return TRUE;

		case IDC_STOP:

			/* update flag so that code can act accordingly on the next audio event */
			bActive = FALSE;

			return TRUE;

		case IDC_CHOOSE_IN:

			/* handle events from audio input selection control */
			switch (HIWORD(wParam))
			{
			case CBN_DROPDOWN:
				fill_wavein_combo_box(hwnd);
				break;
			case CBN_CLOSEUP:
				wavein_deviceid = SendMessage(GetDlgItem(hwnd, IDC_CHOOSE_IN), CB_GETCURSEL, 0, 0);
				if (-1 == wavein_deviceid)
				{
					SendMessage(GetDlgItem(hwnd, IDC_CHOOSE_IN), CB_SETCURSEL, 0, 0);
					wavein_deviceid = 0;
				}
				break;
			}

			return TRUE;

		case IDC_CHOOSE_OUT:

			/* handle events from audio output selection control */
			switch (HIWORD(wParam))
			{
			case CBN_DROPDOWN:
				fill_waveout_combo_box(hwnd);
				break;
			case CBN_CLOSEUP:
				waveout_deviceid = SendMessage(GetDlgItem(hwnd, IDC_CHOOSE_OUT), CB_GETCURSEL, 0, 0);
				if (-1 == waveout_deviceid)
				{
					SendMessage(GetDlgItem(hwnd, IDC_CHOOSE_OUT), CB_SETCURSEL, 0, 0);
					waveout_deviceid = 0;
				}
				break;
			}
			return TRUE;
		}

		break;

	case MM_WIM_OPEN:

		/* audio input is now active; grey-out appropriate controls */

		EnableWindow (GetDlgItem (hwnd, IDC_START), FALSE);
		EnableWindow (GetDlgItem (hwnd, IDC_STOP), TRUE) ;
		EnableWindow (GetDlgItem (hwnd, IDC_CHOOSE_IN), FALSE);
		EnableWindow (GetDlgItem (hwnd, IDC_CHOOSE_OUT), FALSE);
		SetFocus (GetDlgItem (hwnd, IDC_STOP));

		/* add the buffers */

		waveInAddBuffer (hWaveIn, pWaveHdr1, sizeof (WAVEHDR));
		waveInAddBuffer (hWaveIn, pWaveHdr2, sizeof (WAVEHDR));

		/* update flag */
		bActive = TRUE;

#ifdef LOG_AUDIO
		fd = fopen("rec.raw", "wb");
#endif

		/* start sampling */
		waveInStart (hWaveIn);

		return TRUE;

	case MM_WIM_DATA:

		/* input audio has arrived; set pointer to WAVEHDR that describes the audio */
		pWaveParmHdr = ((PWAVEHDR) lParam);
		
		memcpy(&(chan.listen_buffer[chan.write_index]), pWaveParmHdr->lpData, pWaveParmHdr->dwBytesRecorded);
		chan.write_index += pWaveParmHdr->dwBytesRecorded / sizeof(short);

		/* perform the demodulation */
		while ( (chan.read_index + MAX_DEMOD_SAMPLES) < chan.write_index )
		{
			index = chan.read_index;

			chan.demod->demodulate(modem_ctx);

			if (chan.laden)
			{
				/* allocate memory to store the Codec2 encoded/decoded result */
				pWaveOutHdr = malloc (sizeof (WAVEHDR));
				pWaveOutHdr->lpData = malloc(audio_buffer_size);

				/* prepare the header for the waveform output */
				pWaveOutHdr->dwBufferLength  = audio_buffer_size;
				pWaveOutHdr->dwBytesRecorded = 0;
				pWaveOutHdr->dwUser          = 0;
				pWaveOutHdr->dwFlags         = 0;
				pWaveOutHdr->dwLoops         = 1;
				pWaveOutHdr->lpNext          = NULL;
				pWaveOutHdr->reserved        = 0;

				/* set the begin and end flags appropriately */
				if (0 == iteration++)
					pWaveOutHdr->dwFlags |= WHDR_BEGINLOOP;
				if (!bActive)
					pWaveOutHdr->dwFlags |= WHDR_ENDLOOP;

				i = 0;
				c2out = (short *)pWaveOutHdr->lpData;
				c2in = chan.c2_speech;
				while (i < (audio_buffer_size / sizeof(short)))
				{
					*c2out++ = *c2in;
					if (0 == (++i % (OFDM_SAMPLE_RATE / 8000)))
						c2in++;
				}

				/* send the output audio for playback */
				waveOutPrepareHeader (hWaveOut, pWaveOutHdr, sizeof (WAVEHDR));
				waveOutWrite (hWaveOut, pWaveOutHdr, sizeof (WAVEHDR));

				chan.laden = 0;
			}
		}

		memmove(chan.listen_buffer, &(chan.listen_buffer[chan.read_index]), (chan.write_index - chan.read_index) * sizeof(short));
		chan.write_index -= chan.read_index;
		chan.read_index = 0;

		SetWindowText(GetDlgItem(hwnd, IDC_STATUS), (chan.dcd) ? "RECEIVING" : "");

		/* if we are no longer active, close the input audio */
		if (!bActive)
		{
			waveInClose (hWaveIn);
			iteration = 0;
			return TRUE;
		}

		/* if we've reached here, we must be still active, so recycle the buffer by offering it back to the soundcard */
		waveInAddBuffer (hWaveIn, pWaveParmHdr, sizeof (WAVEHDR));

		return TRUE;

	case MM_WIM_CLOSE:
		
		/* audio input has closed, so free the buffer memory */
		waveInUnprepareHeader (hWaveIn, pWaveHdr1, sizeof (WAVEHDR));
		waveInUnprepareHeader (hWaveIn, pWaveHdr2, sizeof (WAVEHDR));
		free (pBuffer1);
		free (pBuffer2);

		/* grey-out appropriate controls */
		EnableWindow (GetDlgItem (hwnd, IDC_START), TRUE);
		EnableWindow (GetDlgItem (hwnd, IDC_STOP), FALSE);
		EnableWindow (GetDlgItem (hwnd, IDC_CHOOSE_IN), TRUE);
		EnableWindow (GetDlgItem (hwnd, IDC_CHOOSE_OUT), TRUE);
		SetFocus (GetDlgItem (hwnd, IDC_START));

#ifdef LOG_AUDIO
		fclose(fd);
#endif

		return TRUE;

	case MM_WOM_OPEN:

		/* audio output has been opened */

		return TRUE;

	case MM_WOM_DONE:

		/* soundcard is done with the output buffer provided, so we free the memory */
		pWaveParmHdr = ((PWAVEHDR) lParam);
		waveOutUnprepareHeader (hWaveOut, pWaveParmHdr, sizeof (WAVEHDR));
		free(pWaveParmHdr->lpData);

		/* if we are no longer active, close the output audio */
		if (!bActive)
		{
			waveOutClose (hWaveOut);
		}

		return TRUE;

	case MM_WOM_CLOSE:

		/* audio output has been closed */
		
		/* if we are terminating, we send ourself a message to close up */
		if (bTerminating)
			SendMessage (hwnd, WM_SYSCOMMAND, SC_CLOSE, 0L);

		return TRUE;

	case WM_SYSCOMMAND:

		switch (LOWORD (wParam))
		{
		case SC_CLOSE:

			/* if we are still active, we update the flags and allow the software to close shop gracefully */
			if (bActive)
			{
				bTerminating = TRUE;
				bActive = FALSE;
				return TRUE;
			}

			/* we must not be active, so we can free memory and end the program */
			free (pWaveHdr1);
			free (pWaveHdr2);
			free (chan.c2_frame);
			free (chan.c2_speech);
			codec2_destroy(codec2_ctx);

			EndDialog (hwnd, 0);

			return TRUE;
		}
		break;
	}
	return FALSE;
}

/*
an oddity of Windows is that apparently there is no nice way to resize a drop-down combo box to reflect the number of entries
this is the only way (however kludgy) that I know of to do this
*/

static void resize_combo_box(HWND hwnd, int identifier, int count)
{
	RECT LboxRc;
	int Height;
	HWND handle;

	handle = GetDlgItem(hwnd, identifier);

	if (handle)
	{
		GetWindowRect(handle, &LboxRc);

		Height = SendMessage(handle, CB_GETITEMHEIGHT, 0, 0) * count;
		Height += SendMessage(handle, CB_GETITEMHEIGHT, -1, 0);
		Height += GetSystemMetrics(SM_CYEDGE) * 2;
		Height += GetSystemMetrics(SM_CYEDGE) * 2;

		SetWindowPos(handle, NULL, 0, 0, LboxRc.right - LboxRc.left, Height, SWP_NOMOVE | SWP_NOZORDER );
	}
}

/* fill in all the audio input choices */

static void fill_wavein_combo_box(HWND hwnd)
{
	WAVEINCAPS wic;
	int iNumDevs, i;

	iNumDevs = waveInGetNumDevs();

	SendMessage(GetDlgItem(hwnd, IDC_CHOOSE_IN), CB_RESETCONTENT, 0, 0);

	SendMessage(GetDlgItem(hwnd, IDC_CHOOSE_IN), CB_ADDSTRING, 0, (LPARAM)"default input");

	for (i = 0; i < iNumDevs; i++)
	{
		if (!waveInGetDevCaps(i, &wic, sizeof(WAVEINCAPS)))
		{
			SendMessage(GetDlgItem(hwnd, IDC_CHOOSE_IN), CB_ADDSTRING, 0, (LPARAM)wic.szPname);
		}
	}

	resize_combo_box(hwnd, IDC_CHOOSE_IN, iNumDevs + 1);
}

/* fill in all the audio output choices */

static void fill_waveout_combo_box(HWND hwnd)
{
	WAVEOUTCAPS wic;
	int iNumDevs, i;

	iNumDevs = waveOutGetNumDevs();

	SendMessage(GetDlgItem(hwnd, IDC_CHOOSE_OUT), CB_RESETCONTENT, 0, 0);

	SendMessage(GetDlgItem(hwnd, IDC_CHOOSE_OUT), CB_ADDSTRING, 0, (LPARAM)"default output");

	for (i = 0; i < iNumDevs; i++)
	{
		if (!waveOutGetDevCaps(i, &wic, sizeof(WAVEOUTCAPS)))
		{
			SendMessage(GetDlgItem(hwnd, IDC_CHOOSE_OUT), CB_ADDSTRING, 0, (LPARAM)wic.szPname);
		}
	}

	resize_combo_box(hwnd, IDC_CHOOSE_OUT, iNumDevs + 1);
}

void audioread(struct modemchannel *chan, int16_t *samples, unsigned int nr, u_int16_t tim)
{
	unsigned int i;
	static int idx;
	static short val;
	static int test_index;

	for (i = 0; i < nr; i++)
	{
		samples[i] = chan->listen_buffer[chan->read_index++];
	}

#ifdef LOG_AUDIO
	fwrite(samples, nr, sizeof(short), fd);
#endif

	chan->pos = tim;
}

void putwords(struct modemchannel *chan, const unsigned *words, unsigned int len)
{
	assert(2 == len);

	/* recover the 50 bits of Codec2 data */
	chan->c2_frame[0] = (u_int8_t)((words[0] & 0x01FE0000) >> 17);
	chan->c2_frame[1] = (u_int8_t)((words[0] & 0x0001FE00) >> 9);
	chan->c2_frame[2] = (u_int8_t)((words[0] & 0x000001FE) >> 1);
	chan->c2_frame[3] = (u_int8_t)((words[1] & 0x01FE0000) >> 17);
	chan->c2_frame[4] = (u_int8_t)((words[1] & 0x0001FE00) >> 9);
	chan->c2_frame[5] = (u_int8_t)((words[1] & 0x000001FE) >> 1);
	chan->c2_frame[6] = (words[0] & 1) ? 0x80 : 0x00;
	chan->c2_frame[6] |= (words[1] & 1) ? 0x40 : 0x00;

	/* decode the Codec2 data into speech*/
	codec2_decode(codec2_ctx, chan->c2_speech, chan->c2_frame);

	chan->laden = 1;
}

/* library expects logprintf()*/

void logprintf(unsigned int level, const char *fmt, ...)
{
}

/* the library provides a Data Carrier Detect signal */

void pktsetdcd(struct modemchannel *chan, int dcd)
{
	chan->dcd = dcd;
}

/* the library ostensibly has provision to be aware of discontinuities in the audio stream */

u_int16_t audiocurtime(struct modemchannel *chan)
{
	return chan->pos;
}
